using System;
using System.Collections.Generic;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Text.RegularExpressions;
/*

TODO:
 Work any snags out of the gui
 update the tool tip help
 update user guide text and screenshot.
*/
namespace DbView
{

	public partial class ViewFilterForm : Form
	{
		ViewDefinition viewDefinition;
		string table;
		string clause;
		string matchString;
		ArrayList tableFieldList;
		string initialField;
		string initialValue;
		int emptyIndex = 0;
		int programaticTextChange = 0; // subtle control issues
		string lastFilter="";		// used for detecting when to synch the filter text to the search clause
		bool dirty = false;

		public ViewFilterForm(ViewDefinition viewDefinition, string initialField, string initialValue)
		{
			InitializeComponent();
			this.lblHelp.Text = helpText;
			this.viewDefinition = viewDefinition;
			this.table = this.viewDefinition.TableName;
			this.initialField = initialField;
			this.initialValue = initialValue.TrimEnd();
			this.clause = this.viewDefinition.FilterClause;
			// 		
			this.comboConditions.Items.Add(new Condition(Condition.op.Equal));
			this.comboConditions.Items.Add(new Condition(Condition.op.NotEqual));
			this.comboConditions.Items.Add(new Condition(Condition.op.Empty));
			this.emptyIndex = this.comboConditions.Items.Count - 1;
			this.comboConditions.Items.Add(new Condition(Condition.op.NotEmpty));
			this.comboConditions.Items.Add(new Condition(Condition.op.InList));
			this.comboConditions.Items.Add(new Condition(Condition.op.NotInList));
			this.comboConditions.Items.Add(new Condition(Condition.op.StartsWith));
			this.comboConditions.Items.Add(new Condition(Condition.op.Contains));
			this.comboConditions.Items.Add(new Condition(Condition.op.DoesNotContain));
			this.comboConditions.Items.Add(new Condition(Condition.op.Between));
			this.comboConditions.Items.Add(new Condition(Condition.op.GreaterThan));
			this.comboConditions.Items.Add(new Condition(Condition.op.GreaterThanOrEqual));
			this.comboConditions.Items.Add(new Condition(Condition.op.LessThan));
			this.comboConditions.Items.Add(new Condition(Condition.op.LessThanOrEqual));

			// help
			ToolTip toolTipHelp = new ToolTip();

			// Set up the delays for the ToolTip.
			toolTipHelp.AutoPopDelay = 20000;
			toolTipHelp.InitialDelay = 250;
			toolTipHelp.ReshowDelay = 100;
			toolTipHelp.ShowAlways = true;
			// Force the ToolTip text to be displayed whether or not the form is active.
			ConfigureToolTipHelp(toolTipHelp, HELP_STRING);
			// tie to the check box
			chkHelp.Tag = toolTipHelp;
			SynchronizeToolTipState(chkHelp);
		}

		public string Clause
		{
			get { return this.clause; }
		}

		/*
				// use this to leverage the logic behind the form without 
				// showing the form
				public string ClauseFromInitialInput()
				{
					string field = this.m_InitialField;
					string comp = "=";
					string rhs = makeRhs(this.m_InitialValue);

					return string.Format("{0} {1} {2}", field, comp, rhs);
				}
		*/
		// logic
		SchemaField GetField()
		{
			int idx = this.listFields.SelectedIndex;
			if (idx == -1)
				return null;
			string fieldName = listFields.Items[idx].ToString();
			foreach (SchemaField f in this.viewDefinition.SchemaFields)
				if (fieldName == f.Name) return f;
			return null;
		}

		string ClauseFromComplex()
		{
			return this.textComplex.Text;
		}

		private bool NumberTest(String test)
		{
			// allows:  99, >99 <99 >=99, <=99, 99-101
			string pattern = @"^(\d+)$";
			// regex object contrstucet with pattern
			Regex r = new Regex(pattern);
			Match m = r.Match(test);
			// if match
			if (m.Success)
				return true;
			return false;
		}

		string MakeRhs(String text)
		{
			// test whether user has decorated the clause with ''
			if (text.IndexOf('\'') >= 0)
				return text;
			// test whether numeric and return unmodified
			if (NumberTest(text) == true)
				return text;
			return string.Format("\'{0}\'", text);
		}

		string MakeRhs(String comp, string text)
		{
			// 
			if (comp == "in")
			{
				string clause = "(";
				String[] seekList = text.Split(',');
				for (int idx = 0; idx < seekList.Length; ++idx)
				{
					if (idx > 0) clause += ",";
					clause += MakeRhs(seekList[idx]);
				}
				clause += ")";
				return clause;
			}
			else
				return MakeRhs(text);
		}

		string ClauseFromSimple(bool showError)
		{
			SchemaField field = GetField();
			Condition op = this.comboConditions.Items[this.comboConditions.SelectedIndex] as Condition;
			string instr = this.textSimple.Text;
			// validate
			if (!ValidatesOk(field, op, instr, showError))
				return "";
			string comp = op.Sql;
			string rhs = op.FormatRhs(this.textSimple.Text, field);// makeRhs(comp, this.textSimple.Text);
			return string.Format("{0} {1} {2}", SafeSqlField(field.Name), comp, rhs);
		}

		private static String SafeSqlField(String field)
		{
			if (field.Length == 0)
				return field;
			if (field[0] == '[' && field[field.Length - 1] == ']')
				return field;
			// hacked fix for aliased fields.
			if (field.IndexOf('.') > 0)
				return field;
			return "[" + field.Trim() + "]"; // does not allow for fields with leading & trailing spaces
		}

		private void SetDirtyFlag(bool tf)
		{
			this.dirty = tf;
			bool filterHasText = !string.IsNullOrEmpty(this.textComplex.Text);
			this.AcceptButton = (this.dirty) ? this.btnAdd : (filterHasText) ? this.btnOkComplex : this.btnOkSimple;
		}

		private void SynchFilterFromClause(bool initialising)
		{
			// rules for synching the search clause to the filter text
			bool wantSync = (initialising ||								// initial setting or.
							this.textComplex.Text == this.lastFilter &&	// filter is matches controls before current change
							!string.IsNullOrEmpty(this.lastFilter));	// and the hold string has already been set by the initialisation stage
			// 
			wantSync = false; // new design: we never want to sync
			System.Drawing.Color filterFontColour = System.Drawing.Color.Black;
			if (wantSync)
			{
				// see whether relevant
				this.lastFilter = this.ClauseFromSimple(false);
				this.textComplex.Text = this.lastFilter;
				filterFontColour = System.Drawing.Color.Gray;
			}
			// set add to filter button depending on sync state
			// ie. if the controls are in sync with the filter then there is no need for 
			// adding. Indeed it is detrimental
			// not sure
			// this.btnAdd.Enabled = !wantSync; // ie. button enabled when not in sync
			// instead
			this.textComplex.ForeColor = filterFontColour;
		}

		bool ValidatesOk(SchemaField field, Condition op, string rhs, bool showError)
		{
			if (op.Op != Condition.op.Empty && op.Op != Condition.op.NotEmpty && field.Type != "S" && rhs.Trim().Length == 0)
				return ValidateError("The filter requires something to test against", showError);
			if (op.Op == Condition.op.Between)
			{
				string low, high;
				if (Condition.TryParseBetween(rhs, out low, out high) == false)
					return ValidateError("Could not establish the search range. Separate low and high values with space, comma or the word AND.", showError);
			}
			if (field.Type != "S")
				if (op.Op == Condition.op.StartsWith ||
					op.Op == Condition.op.Contains ||
					op.Op == Condition.op.DoesNotContain)
					return ValidateError(String.Format("The Search condition {0} is not valid for the type of field selected", op.ToString()), showError);
			return true;
		}

		private bool ValidateError(string message, bool showError)
		{
			if (showError)
				MessageBox.Show(message, "Filter error", MessageBoxButtons.OK, MessageBoxIcon.Error);
			return false;
		}

		private void GetSchemaInfo()
		{
			SchemaField[] schema = this.viewDefinition.SchemaFields;
			// modify yp 
			this.tableFieldList = new ArrayList();
			foreach (SchemaField field in schema)
				this.tableFieldList.Add(field.Name);
		}
		private bool Matches(String target, string pattern)
		{
			if (pattern.Length == 0) // no search set
				return true;
			// simple contains
			if (target.ToUpper().IndexOf(pattern.ToUpper()) >= 0)
				return true;
			return false;
		}
		private void PopulateFields(String strMatch)
		{
			listFields.Items.Clear();
			if (strMatch.Length > 0)
				listFields.ForeColor = System.Drawing.Color.Red;
			else
				listFields.ForeColor = System.Drawing.SystemColors.WindowText;
			foreach (String fieldName in this.tableFieldList)
			{
				if (Matches(fieldName, strMatch))
					listFields.Items.Add(fieldName);
			}
			// set 
			if (listFields.Items.Count > 0)
				listFields.SelectedIndex = 0;
		}

		void SetFieldSelection(String field)
		{
			// set index for no match condition
			int sel = (listFields.Items.Count > 0) ? 0 : -1;
			for (int idx = 0; idx < listFields.Items.Count; ++idx)
			{
				if (listFields.Items[idx].ToString().CompareTo(field) == 0)
				{
					sel = idx;
					break;
				}
			}
			listFields.SelectedIndex = sel;
		}

		private void setMatchString(String NewMatch)
		{
			if (NewMatch.CompareTo(this.matchString) == 0)
				return; // do nothing if nothing to do.
			this.matchString = NewMatch;
			PopulateFields(this.matchString);
			// only enable reset when there is something to reset to
			btnAll.Enabled = (this.matchString.Length > 0);
			// hook the escape key if the filter text is not empty so escape can reset the list.
			this.CancelButton = (string.IsNullOrEmpty(this.matchString)) ? this.btnCancel : null;
		}

		private void listFields_KeyPress(object sender, System.Windows.Forms.KeyPressEventArgs e)
		{
			char c = e.KeyChar;
			if ((c >= '0' && c <= '9')
				|| (c >= 'A' && c <= 'Z')
				|| (c >= 'a' && c <= 'z'))
			{
				setMatchString(this.matchString + c);
				e.Handled = true;
			}
			else if (c == 8)
			{
				int l = this.matchString.Length;
				if (l > 0)
				{
					setMatchString(this.matchString.Substring(0, l - 1));
					e.Handled = true;
				}
			}
			else if (c == 27)
			{
				setMatchString("");
				e.Handled = true;
			}
		}

		private void ViewFilterForm_Load(object sender, EventArgs e)
		{
			++programaticTextChange;
			this.lastFilter = ""; // initialise the filter created from the clause controls.
			GetSchemaInfo();
			PopulateFields("");
			SetFieldSelection(this.initialField);
			this.textSimple.Text = this.initialValue;
			// initlialise the condition to = unless the cell selected is empty
			this.comboConditions.SelectedIndex = (this.initialField.Length > 0 && this.initialValue.Length == 0) ? this.emptyIndex : 0;
			
			// set to the initial field
			bool initialDirtyState = false;			// dirty state logic for correct default button selection
													// if no existing filter is best to set this false
			if (string.IsNullOrEmpty(this.clause))
				this.SynchFilterFromClause(true);	// invoke with the true parameter to start the sync process.
													// Only call this when all controls have been initialised.
			else
			{
				this.textComplex.Text = this.clause;
				initialDirtyState = true;			// this flips default behaviour to adding the new clause to the existing one
			}
			SetDirtyFlag(initialDirtyState); // no edits yet. Controls the default button or enter key behaviour
			--programaticTextChange;
		}

		private void btnCancel_Click(object sender, EventArgs e)
		{
			DialogResult = DialogResult.Cancel;
			this.Close();
		}

		private void btnOkSimple_Click(object sender, EventArgs e)
		{
			// clause will be empty if entry is not valid
			string clause = this.ClauseFromSimple(true);
			if (clause.Length > 0)
			{
				// will check if there is stuff in the full filter.
				if (ConfirmOkToProceed(clause))
				{
					this.clause = clause;
					this.viewDefinition.FilterClause = this.clause;
					DialogResult = DialogResult.OK;
					this.Close();
				}
			}
		}

		private bool ConfirmOkToProceed(string clause)
		{
return true;
			// Good to go in simple scenario
			if (this.textComplex.Text.Length == 0) 
			{
				return true;
			}
			// There is something built in the full filter
			String message = 
@"You are about to apply a single search clause as the filter. This
will ignore the clause or clauses in the filter box. If that is OK
then press [ Yes ] to proceed. If you intended to add to the filter box
then press [ No ] then [ Add to Filter ] and continue or [ Apply ].

Do you wish to proceed?";
			return (MessageBox.Show(message, "Please Confirm", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == System.Windows.Forms.DialogResult.Yes);
		}

		private void btnOkComplex_Click(object sender, EventArgs e)
		{
			DialogResult = DialogResult.OK;
			this.clause = this.ClauseFromComplex();
			this.viewDefinition.FilterClause = this.clause;
			this.Close();
		}

		private void btnRemove_Click(object sender, EventArgs e)
		{
			DialogResult = DialogResult.OK;
			this.clause = "";
			this.viewDefinition.FilterClause = this.clause;
			this.Close();

		}
		private void btnAll_Click(object sender, EventArgs e)
		{
			setMatchString("");
		}

		private void textSimple_TextChanged(object sender, EventArgs e)
		{
			//this.AcceptButton = this.btnOkSimple;
			// false prevents accidents when events are fired as the form is being loaded
			this.SynchFilterFromClause(false);
			SetDirtyFlag(true);
		}

		private void textComplex_TextChanged(object sender, EventArgs e)
		{
			if (this.programaticTextChange == 0)
				this.AcceptButton = this.btnOkComplex;
		}

		private void comboConditions_SelectedIndexChanged(object sender, EventArgs e)
		{
			DoBetweenCheck();
			// false prevents accidents when events are fired as the form is being loaded
			this.SynchFilterFromClause(false);
			SetDirtyFlag(true);
		}

		private void DoBetweenCheck()
		{
			// if select was switched to Between
			if (this.comboConditions.Text.ToUpper() == "BETWEEN")
			{
				// and there is no " " in the value
				if (!this.textSimple.Text.Contains(" "))
					// duplicate the value so the between is legit
					this.textSimple.Text += " AND " + this.textSimple.Text;
			}
		}

		private void listFields_SelectedIndexChanged(object sender, EventArgs e)
		{
			++this.programaticTextChange;
			string selectedFieldName = this.listFields.Items[this.listFields.SelectedIndex].ToString();
			this.lblField.Text = selectedFieldName;
			// Used to add the field.
			// however since the redesign this is in the way.
			// if no spaces in complex filter field (ie. empty or single field)
			//if (this.textcomplex.text.indexof(' ') == -1)
			//    this.textcomplex.text = selectedfieldname;
			// false prevents accidents when events are fired as the form is being loaded
			this.SynchFilterFromClause(false);
			SetDirtyFlag(true);
			--this.programaticTextChange;
		}

		
		private void listFields_MouseDoubleClick(object sender, MouseEventArgs e)
		{
			// Used to add the field.
			// however since the redesign this is in the way.
			string selectedFieldName = this.listFields.Items[this.listFields.SelectedIndex].ToString();
			// if no spaces in complex filter field (ie. empty or single field)
			if (this.textComplex.Text.IndexOf(' ') == -1 ||
			        string.IsNullOrEmpty(this.textComplex.Text.Trim()) ||
			        LastCharIs(this.textComplex.Text, '('))
			    this.textComplex.Text += selectedFieldName;
			else
			{
			    // detect and 
			    string andText = " and ";
			    if (LastCharsUpperMatch(this.textComplex.Text, "AND") ||
			       LastCharsUpperMatch(this.textComplex.Text, "OR"))
			        andText = ""; // not needed as user has already added it.
			    this.textComplex.Text += andText + selectedFieldName + " ";
			}
			this.textComplex.Focus();
			this.textComplex.SelectionStart = this.textComplex.Text.Length;
		}

		// match last significant character.
		private bool LastCharIs(string target, char c)
		{
			int i = target.Length;
			while (--i > 0)
			{
				if (target[i] == c) 
					return true;
				else if (target[i] != ' ') 
					return false;
			}
			return false;
		}

		private bool LastCharsUpperMatch(string data, string target)
		{
			string s = data.Trim().ToUpper();
			int startPos = s.Length - target.Length; // search start
			if (startPos < 0) return false; // not possible to match
			return (s.Substring(startPos) == target.ToUpper());
		}

		private void btnAdd_Click(object sender, EventArgs e)
		{
			//
			// test whether we are in sync and skip the add 
			//
			String current = this.textComplex.Text;
			String newClause = this.ClauseFromSimple(true);
			// test whether attempting to add the filter when in synch
			if (current == newClause)
				// in effect a nop
				current = newClause;
			else
			{
				// join clauses with AND by default . nb: there is a control to adjust this,
				if (current.Length > 0)
				{
					current += " AND ";
				}
				current += newClause;
			}
			this.textComplex.Text = current;
			// deliberately de-synch
			this.lastFilter = "";
			this.listFields.Focus();
			// call this as it also sets control state
			this.SynchFilterFromClause(false);
			SetDirtyFlag(false);
		}

		private void btnClear_Click(object sender, EventArgs e)
		{
			this.textComplex.Text = "";
		}

		private void btnConnector_Click(object sender, EventArgs e)
		{
			String current = this.textComplex.Text;
			String selected = this.textComplex.SelectedText.Trim().ToUpper();

			string replace = (selected == "AND") ? "OR" :
							 (selected == "OR") ? "AND" : "";
			if (replace.Length > 0)
			{
				int i = 0;
				while (selected[i] == ' ')
				{
					++i;
				}
				current = current.Substring(0, this.textComplex.SelectionStart + i)
							+ replace 
							+ current.Substring(this.textComplex.SelectionStart + i + selected.Length);
				this.textComplex.Text = current;
			}
				
		}
		// () button. surround selected text with ( )
		private void btnBrackets_Click(object sender, EventArgs e)
		{
			// no action if text box is empty (avoid sticking () in the box)
			if (this.textComplex.Text.Length == 0)
			{
				return;
			}
			// assume there is selected text.
			int insertPos2 = this.textComplex.SelectionLength;
			int insertPos1 = this.textComplex.SelectionStart;
			// test assumption
			if (this.textComplex.SelectionLength <= 0) 
			{
				// set to enclose the whole clause in () if no text is selected.
				insertPos1 = 0;
				insertPos2 = this.textComplex.Text.Length;
			}

			// insert in reverse order to preserve the validity of the selected text measures
			String current = this.textComplex.Text.Insert(insertPos1 + insertPos2,")");
			current = current.Insert(insertPos1,"(");

			this.textComplex.Text = current;
		}

		/*
			Expecting a string of form
			Control Name
			Help
			[More help]
			%%%
		*/
		void ConfigureToolTipHelp(ToolTip toolTipHelp, string helpString)
		{
			String[] helpLines = helpString.Split('\n');
			int state = 0;
			string controlName = "";
			string controlHelp = "";
			foreach (String helpLine in helpLines)
			{
				string line = helpLine.Trim();
				switch (state)
				{
					case 0:
						if (line.Length > 0)
						{
							controlName = line;
							state = 1;
						}
						break;
					case 1:
						if (line == "%%%")
						{
							//Control c = this.Controls[controlName];
							Control c = FindControl(this, controlName);
							toolTipHelp.SetToolTip(c, controlHelp);
							controlHelp = "";
							state = 0;
						}
						else
						{
							if (controlHelp.Length > 0) controlHelp += "\n";
							controlHelp += line;
						}
						break;
				}
			}
		}

		private Control FindControl(Control parent, string controlName)
		{
			// not a control collection (makes recursive use easier)
			if (parent == null)
				return null;
			// find or dig deeper
			foreach (Control c in parent.Controls)
			{
				if (c.Name == controlName)
				{
					return c;
				}
				else
				{
					Control found = FindControl(c, controlName);
					if (found != null)
						return found;
				}
			}
			return null;
		}


		private void chkHelp_CheckedChanged(object sender, EventArgs e)
		{
			SynchronizeToolTipState(sender as CheckBox);
		}

		private void SynchronizeToolTipState(CheckBox ch)
		{
			// assume the tooltip is tied to the check box that controls activation
			ToolTip tt = ch.Tag as ToolTip;
			// if it's a tooldtip sync active state to checkbox state.
			if (tt != null)
				tt.Active = ch.Checked;
		}


		const string helpText = "Set or clear a filter clause for the grid data. Define a single field filter in the Single search "+
								"filter box and either apply it directly or use it to build a multi-field filter.\n"+
								"For further help click on the checkbox at the bottom of the screen to activate tool tip help for each control";
		const string HELP_STRING = @"
listFields
Displays the columns of the table. Click on or navigate with 
the arrow keys to select the field to search on. When the list
has focus typing letters will apply a filter to the list
and change the text to red. Backspace will undo the effect
of the previous letter typed. Selecting a field or fields 
will restore the full list. Double-click to select
the current field into the filter box. 
Restore the full list by pressing the [Reset list] button.
%%%
comboConditions
Contains a list of Sql comparison operators to select for the 
current clause. The default will be ""equal to"" if the selected 
grid cell contains data. If the cell is empty then the comparison 
is set to ""is null"".
%%%
btnOkSimple
Performs the single search using the clause above the button.
If you wish to apply the filter clause below this then press
the [ Apply ] button above the filter description.
%%%
btnOkComplex
Applies the filter clause described in the box below to the grid data.
%%%
btnCancel
Close the dialog discarding any new filter or changes to the existing one.
%%%
textSimple
Enter the value to compare to the data. You do not need to add 
quotes for text and date fields, but date fields and number fields
must be a valid format otherwise there will be a Sql error.
%%%
textComplex
Build a multi-field filter in this box either by combining several
Single searches using the [ Add to Filter ] button above or by typing 
directly into the box. 
%%%
btnAll
Use this button to return a filtered field list to the full list. 
The text will turn from red to black in a visual confirmation
of the action.
%%%
btnRemove
Clears any outstanding filter and redisplays all the rows in the grid.
%%%
lblField
Read-only display confirming the column selected for the 
current single search.
%%%
btnAdd
Press this to add the current single filter to the filter box. The
clause is joined to any other with an ""AND"" connector but you may
change this either by typing directly or with the [ And/ Or ] button.
%%%
btnClear
Press this button to clear the filter. Unlike pressing 
the [ Remove filter ] button this will not close the dialog.
%%%
btnConnector
To use this button correctly mark any ""AND"" or ""OR"" connectors in
the filter box before pressing it. The marked text will toggle from 
""AND"" to ""OR"" or ""OR"" to ""AND"" 
%%%
btnBrackets
To use this button correctly mark a pair (or more) of the SQL clauses
in the filter box before pressing it. The marked text will then
be enclosed with a pair of brackets altering the precedence of the 
search.
%%%
chkHelp
Enables tool tip help.
%%%

";


	}

	// I know this should be bunch of classes with inherited stuff
	class Condition
	{
		internal enum op
		{
			Equal, NotEqual, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual,
			InList, NotInList, Contains, DoesNotContain, StartsWith, Between,
			Empty, NotEmpty
		};

		internal Condition(op opIn)
		{
			this.m_Op = opIn;
		}

		op m_Op;

		internal op Op { get { return this.m_Op; } }

		internal string Sql
		{
			get
			{
				return (this.m_Op == op.Equal) ? "=" :
						(this.m_Op == op.NotEqual) ? "!=" :
						(this.m_Op == op.InList) ? "in" :
						(this.m_Op == op.NotInList) ? " not in" :
						(this.m_Op == op.GreaterThan) ? ">" :
						(this.m_Op == op.GreaterThanOrEqual) ? ">=" :
						(this.m_Op == op.LessThan) ? "<" :
						(this.m_Op == op.LessThanOrEqual) ? "<=" :
						(this.m_Op == op.Empty) ? " is " :
						(this.m_Op == op.NotEmpty) ? " is not " :
						(this.m_Op == op.Contains) ? " like " :
						(this.m_Op == op.DoesNotContain) ? " not like " :
						(this.m_Op == op.StartsWith) ? " like " :
						(this.m_Op == op.Between) ? " between " :
						this.m_Op.ToString();
			}
		}

		public override string ToString()
		{
			return (this.m_Op == op.Equal) ? "Equal To" :
					(this.m_Op == op.NotEqual) ? "Not Equal To" :
					(this.m_Op == op.InList) ? "In List" :
					(this.m_Op == op.NotInList) ? "Not In List" :
					(this.m_Op == op.GreaterThan) ? "Greater than" :
					(this.m_Op == op.GreaterThanOrEqual) ? "Greater Than or Equal" :
					(this.m_Op == op.LessThan) ? "Less than" :
					(this.m_Op == op.LessThanOrEqual) ? "Less Than or Equal" :
					(this.m_Op == op.Empty) ? "Is Null" :
					(this.m_Op == op.NotEmpty) ? "Is Not Null" :
					(this.m_Op == op.Contains) ? "Contains" :
					(this.m_Op == op.DoesNotContain) ? "Does not contain" :
					(this.m_Op == op.StartsWith) ? "Starts with" :
					(this.m_Op == op.Between) ? "Between" :
				   this.m_Op.ToString();
		}

		internal string FormatRhs(String rhs, SchemaField f)
		{
			// NULL/Non-NULL no matter what has been entered.
			if (this.m_Op == op.NotEmpty || this.m_Op == op.Empty) return "NULL";

			// others
			bool isNumeric = (f.Type == "I" || f.Type == "N");
			string outstr = rhs;
			if (this.m_Op == op.InList || this.m_Op == op.NotInList)
			{
				String[] items = rhs.Split(',');
				outstr = "(";
				string comma = "";
				foreach (String s in items)
				{
					outstr += comma + ((isNumeric) ? s : "'" + s + "'");
					comma = ",";
				}
				outstr += ")";
			}
			else if (this.m_Op == op.Between)
			{
				string low, high;
				if (TryParseBetween(rhs, out low, out high))
				{
					low = (isNumeric) ? low : "'" + low + "'";

					// Add a high val
					if (f.Type == "S") high += "z";

					high = (isNumeric) ? high : "'" + high + "'";
					outstr = low + " and " + high;
				}
			}
			else
			{
				switch (this.m_Op)
				{
					case op.StartsWith: outstr += "%"; break;
					case op.Contains: outstr = "%" + outstr + "%"; break;
					case op.DoesNotContain: outstr = "%" + outstr + "%"; break;
					default: outstr = rhs;
						break;
				}
				outstr = (isNumeric) ? outstr : "'" + outstr + "'";
			}
			return outstr;
		}

		internal static bool TryParseBetween(String rhs, out string low, out string high)
		{
			string sep = " AND ";
			if (BreakWithSep(rhs, sep, out low, out high)) return true;
			sep = ",";
			if (BreakWithSep(rhs, sep, out low, out high)) return true;
			sep = " ";
			if (BreakWithSep(rhs, sep, out low, out high)) return true;
			return false;
		}

		private static bool BreakWithSep(String rhs, string sep, out string low, out string high)
		{
			low = ""; high = "";
			string normal = rhs.Trim().Replace("    ", "  ").Replace("  ", " "); // try to allow for a few space removals
			int iPos = normal.ToUpper().IndexOf(sep.ToUpper());
			if (iPos == -1) return false;
			// below does not allow low  high
			if (normal.ToUpper().LastIndexOf(sep.ToUpper()) != iPos) return false; // 2 separators = 3 clauses.
			low = normal.Substring(0, iPos);
			high = normal.Substring(iPos + sep.Length);
			return true;
		}
	}
}